package au.com.zonski.gwlite.j2me.client;

import java.util.Random;
import java.util.Vector;

import javax.microedition.lcdui.Font;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.lcdui.game.Sprite;

import net.jscience.math.kvm.MathFP;

public class GWLiteCanvas 
 	extends GameCanvas
	implements Runnable 
{
	private static final int DEFAULT_TARGET_FPS = 20;
	private static final int DEFAULT_LEVEL_WIDTH = MathFP.toFP(256);
	private static final int DEFAULT_LEVEL_HEIGHT = MathFP.toFP(256);
	
	private static final Font SCORE_FONT				= Font.getFont( Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_MEDIUM );
	private static final Font INFO_FONT					= Font.getFont( Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_SMALL );
	private static final Font MAIN_FONT					= Font.getFont( Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_MEDIUM );
	
	public static final byte PLAYER_LEVEL				= GWEntity.TYPE_PLAYER;
	public static final byte BULLET_LEVEL				= GWEntity.TYPE_BULLET;
	
	public static final byte UPDATE_PAUSE				= 30;
	
	public static final byte WALL_WEST 					= 1;
	public static final byte WALL_EAST					= 2;
	public static final byte WALL_NORTH					= 4;
	public static final byte WALL_SOUTH					= 8;
	
	private static final byte SPRITE_WIDTH				= 0;
	private static final byte SPRITE_HEIGHT				= 1;
	private static final byte SPRITE_NUMBER_OF_TILES	= 2;
	private static final byte SPRITE_TRANSFORM			= 3;
	
	private static final int MAX_SPAWN_DELAY			= 10 * DEFAULT_TARGET_FPS;
	private static final int MIN_SPAWN_DELAY			= (1 * DEFAULT_TARGET_FPS);
	private static final int MIN_SPAWN_BUFFER			= MathFP.toFP( 40 );
	private static final int MAX_SPAWN_DISTANCE			= MathFP.toFP( 50 );
	private static final int MAX_SPAWN_QUANTITY			= 50;
	
	private static final byte SPAWN_STYLE_SURROUND		= 0;
	private static final byte SPAWN_STYLE_NE_CORNER		= 1;
	private static final byte SPAWN_STYLE_SE_CORNER		= 2;
	private static final byte SPAWN_STYLE_NW_CORNER		= 4;
	private static final byte SPAWN_STYLE_SW_CORNER		= 8;
	
	private static final byte SQUARE_SPIN_RADIUS 		= 8;
	private static final byte BUTTERFLY_SPIN_RADIUS		= 3;
	
	private static final byte MODE_INSTRUCTIONS			= -3;
	private static final byte MODE_SCORES				= -2; 
	private static final byte MODE_GAME_OVER			= -1;
	private static final byte MODE_CLICK_TO_PLAY		= 0;
	private static final byte MODE_PLAYING				= 1;
	
	private static final int[] BOMB_COLORS = {
		0x4444AA,
		0x5555BB,
		0x6666CC,
		0x7777DD,
		0x9999FF,
		0x9999FF,
		0x9999FF,
		0x6666CC,
		0x4444AA,
		0x000099,
		0x000088,
		0x000055,
		0x000000
	};
	
	
	public Sprite[][] sprites;
	public int[][] spriteDetails;
	public int[] animationStyle;
	
	public Controller[] controllers;
	public Vector[] layers;
	private int[] layerCollisionMasks;
	private boolean[] layerGravity;
	
	public int updateNumber;
	public int lastDeath;
	private boolean running;
	private int delay = 1000/DEFAULT_TARGET_FPS;
	
	private int wave;
	private int spawnThroughput;
	private int spawnCountDown;
	private int spawnQuantity;
	private int spawnStyle;
	private byte spawnType;
	public Random random = new Random( System.currentTimeMillis() );
	public boolean slowing;
	
	public int viewX = 0;
	public int viewY = 0;
	public int levelWidth = DEFAULT_LEVEL_WIDTH;
	public int levelHeight = DEFAULT_LEVEL_HEIGHT;
	public int screenWidth;
	public int screenHeight;
	
	private Image backgroundImage;
	private int gridSpacing;
	
	public GWEntity blackHole;
	
	private boolean explodeAll;
	private GWEntity explodeExcept;
	
	private PlayerController playerController;
	
	private byte mode;
	
	private int maxIcons = 3;
	
	private int highScore;
	
	public GWLiteCanvas( Image[] wads, int[][] spriteDetails )
	{
		super( false );
		this.setFullScreenMode( true );
		
		int[] animationStyle = new int[ spriteDetails.length ];
		this.spriteDetails = spriteDetails;
		this.sprites = buildSprites( wads, spriteDetails, animationStyle );
		
		SeekerController diamondController = new SeekerController( SeekerController.MODE_SEEK, MathFP.toFP( 2 ) ); 
		this.playerController = new PlayerController( this );
		WormController wormController = new WormController( 
				3, 
				//MathFP.PI/32,
				2,
				3 
		);
		WormController interceptorController = new WormController( 
				5, 
				//MathFP.PI/256,
				2,
				3 
		);
		interceptorController.segments = 0;
		this.controllers = new Controller[]{
			new BulletController( false, 0 ),  				  // bullets
			wormController, 				  				  // worm tail
			new BulletController( true, MathFP.toFP( 1 ) ),   // stars
			diamondController, 								  // diamonds
			new SeekerController( SeekerController.MODE_AVOID, MathFP.toFP( 2 ) + MathFP.toFP( 1 ) / 2 ),// shy squares
			new SeekerController( 
					SeekerController.MODE_SPLIT, 
					MathFP.toFP( 2 ), 
					SQUARE_SPIN_RADIUS 
			),// unbroken square
			new SeekerController( SeekerController.MODE_SEEK, MathFP.toFP( 3 ) ), // circle (seeker)
			wormController, 			  						// worm
			interceptorController, 								// interceptor 
			new SeekerController( 
					SeekerController.MODE_SPIN, 
					MathFP.toFP( 1 ) + MathFP.toFP( 1 ) / 2,
					BUTTERFLY_SPIN_RADIUS
			), // butterfly
			new BlackHoleController( this.sprites[ GWEntity.TYPE_BLACKHOLE ][0].getFrameSequenceLength()-1, 3 ), // black hole
			this.playerController,          				  	// player
			new SeekerController( 
					SeekerController.MODE_SPIN, 
					MathFP.toFP( 1 ),
					SQUARE_SPIN_RADIUS
			), // broken square
			new StunController( 7, false, 1, null ),			// explosions/animations
			new StunController( 30, true, 1, diamondController )// just created
		};
		
		this.layerCollisionMasks = new int[]{
			0x0000, // bullets 
			0x0001, // worm tail
			0x0002, // stars
			0x0004, // diamonds
			0x0008, // shy square
			0x0010, // unbroken square
			0x0020, // circle
			0x0040, // worm
			0x0080, // interceptor
			0x0100, // butterfly
			0x02F7, // black hole
			0x03FF, // player
			0x0803, // broken square
			0x0000, // explosions/animations
			0x2000, // just created
		};
		this.layerGravity = new boolean[]
	    {
			true, 	// bullets 
			false, 	// worm tail
			true, 	// stars
			true, 	// diamonds
			true, 	// shy square
			true, 	// unbroken square
			false, 	// circle
			false, 	// worm
			true, 	// interceptor
			false, 	// butterfly
			false, 	// black hole
			true, 	// player
			true, 	// broken square
			false, // explosions/animations
			false, // just created
	    };
		this.layers = new Vector[this.controllers.length];
		for( int i=this.layers.length; i>0; )
		{
			i--;
			Vector layer = new Vector();
			this.layers[ i ] = layer;
		}
		
		this.animationStyle = animationStyle;
	}
	
	public void buildBackground( Image backgroundTile )
	{
		int width = this.getWidth() + backgroundTile.getWidth();
		int height = this.getHeight() + backgroundTile.getHeight(); 
		Image backgroundImage = Image.createImage( 
				width, height
		);
		Graphics backgroundGraphics = backgroundImage.getGraphics();
		backgroundGraphics.setColor( 0x000000 );
		backgroundGraphics.fillRect( 0, 0, width, height );
		
		// draw some random stars
		/*
		for( int i=0; i<width; i++ )
		{
			int x = Math.abs( this.random.nextInt() % width );
			int y = Math.abs( this.random.nextInt() % height );
			int color = 0xFFFFFF;
			backgroundGraphics.setColor( color );
			backgroundGraphics.drawLine( x, y, x, y );
		}
		*/
		
		for( int x = 0; x<width; x+=backgroundTile.getWidth() )
		{
			for( int y = 0; y<height; y+=backgroundTile.getHeight() )
			{
				backgroundGraphics.drawImage( 
						backgroundTile, x, y, 
						Graphics.TOP | Graphics.LEFT 
				);
			}
		}
		this.backgroundImage = backgroundImage;
		this.gridSpacing = MathFP.toFP( backgroundTile.getWidth() );
	}
	
	private Sprite[][] buildSprites( Image[] wads, int[][] spriteDetails, int[] animationStyle )
	{
		Sprite[][] sprites = new Sprite[ spriteDetails.length ][];
		
		for( int i=0; i<spriteDetails.length; i++ )
		{
			int[] spriteDetail = spriteDetails[i];
			// create an image for this thing
			int spriteTransform = spriteDetail[SPRITE_TRANSFORM]; 
			boolean rotate = spriteTransform > 0;
			Image wad = wads[ i ];
			int spriteWidth = spriteDetail[SPRITE_WIDTH];
			int spriteHeight = spriteDetail[SPRITE_HEIGHT];
			int frames = spriteDetail[SPRITE_NUMBER_OF_TILES];
			int[] frameSequence;
			if( spriteDetail.length > 7 )
			{
				frameSequence = new int[ spriteDetail.length - 7 ];
				System.arraycopy( spriteDetail, 7, frameSequence, 0, frameSequence.length );
			}
			else
			{
				frameSequence = null;
			}
			if( frames > 0 )
			{
				Sprite sprite = new Sprite( wad, spriteWidth, spriteHeight );
				sprite.setFrameSequence( frameSequence );
				sprite.defineReferencePixel( spriteWidth/2, spriteHeight/2 );
				if( rotate )
				{
					// rotate the sprite
					Sprite sprite90 = new Sprite( sprite );
					sprite90.setTransform( Sprite.TRANS_ROT90 );
					Sprite sprite180 = new Sprite( sprite );
					sprite180.setTransform( Sprite.TRANS_ROT180 );
					Sprite sprite270 = new Sprite( sprite );
					sprite270.setTransform( Sprite.TRANS_ROT270 );
					sprites[ i ] = new Sprite[]{ sprite, sprite90, sprite180, sprite270 };
				}
				else
				{
					Sprite staticSprite = new Sprite( sprite );
					staticSprite.setFrameSequence( new int[]{0} );
					sprites[ i ] = new Sprite[]{ sprite, staticSprite };
				}
				animationStyle[ i ] = spriteDetail[SPRITE_TRANSFORM];
			}
		}
//		Sprite bombIcon = sprites[ sprites.length - 2 ][0];
//		bombIcon.setPosition( this.getWidth()/2 - bombIcon.getWidth()*3, 3 );
//		Sprite playerIcon = sprites[ sprites.length - 1 ][0];
//		playerIcon.setPosition( this.getWidth()/2, 3 );
		
		return sprites;
	}
	
	public void start()
	{
		Thread thread = new Thread( this );
		this.mode = MODE_CLICK_TO_PLAY;
		this.running = true;
		this.screenWidth = MathFP.toFP( this.getWidth() );
		this.screenHeight = MathFP.toFP( this.getHeight() );
		thread.start();
		

		int minLevelWidth = MathFP.toFP(this.getWidth());
		int minLevelHeight = MathFP.toFP(this.getHeight());
		if( this.levelWidth < minLevelWidth )
		{
			this.levelWidth = minLevelWidth;
		}
		if( this.levelHeight < minLevelHeight )
		{
			this.levelHeight = minLevelHeight;
		}
		this.viewX = (this.levelWidth - this.screenWidth)/2;
		this.viewY = (this.levelHeight - this.screenHeight)/2;
		
		String maxScoreString = "9999999";
		int maxScoreLength = SCORE_FONT.stringWidth( maxScoreString );
		this.maxIcons = 
			Math.max( 2, 
					(this.getWidth() - maxScoreLength * 2) / (this.sprites[ this.sprites.length - 1 ][0].getWidth() * 2) 
			);
	}
	
	public void stop()
	{
		this.running = false;
	}
	
	public Graphics getGraphics()
	{
		return super.getGraphics();
	}
	
	private void updateGame()
	{
		if( this.spawnCountDown > 0 )
		{
			this.spawnCountDown --;
		}
		else
		{
			if( this.spawnQuantity > 0 )
			{
				if( this.spawnType != GWEntity.TYPE_BLACKHOLE || this.layers[ GWEntity.TYPE_BLACKHOLE ].size() == 0 && this.layers[ this.layers.length - 1 ].size() == 0 )
				{
					int amount = this.spawnThroughput;
					while( this.spawnQuantity > 0 && amount > 0 )
					{
						amount--;
						// select an enemy type
						byte type = this.spawnType;
						
						int width = MathFP.toFP( this.spriteDetails[type][4] );
						int height = MathFP.toFP( this.spriteDetails[type][5] );
						
						this.spawnQuantity --;
						Vector playerLayer = this.layers[ PLAYER_LEVEL ];
						if( playerLayer.size() > 0 )
						{
							GWEntity player = (GWEntity) playerLayer.elementAt( 0 );
							int px = player.x + player.width/2;
							int py = player.y + player.height/2;
							
							int ex;
							int ey;
							boolean surround;
							if( this.spawnStyle != SPAWN_STYLE_SURROUND )
							{
								// choose a corner
								int spawnStyle = this.spawnStyle;
								Vector corners = new Vector( 4 );
								if ( (spawnStyle & SPAWN_STYLE_NE_CORNER ) != 0 )
								{
									corners.addElement( 
											new int[]{
													this.levelWidth - width, 
													height, 
													(MathFP.PI)/2, MathFP.PI} 
									);
								}
								if( (spawnStyle & SPAWN_STYLE_SE_CORNER ) != 0 )
								{
									corners.addElement( 
											new int[]{
													this.levelWidth - width, 
													this.levelHeight - height, 
													MathFP.PI, (MathFP.PI*3)/2} 
									);
									
								}
								if( (spawnStyle & SPAWN_STYLE_NW_CORNER ) != 0 )
								{
									corners.addElement( 
											new int[]{
													width, 
													height, 
													0, MathFP.PI/2} 
									);
									
								}
								if( (spawnStyle & SPAWN_STYLE_SW_CORNER ) != 0 )
								{
									corners.addElement( 
											new int[]{
													width, 
													this.levelHeight - height, 
													(MathFP.PI*3)/2, MathFP.PI*2} 
									);
								}
								int[] spawnDetails = (int[]) corners.elementAt( this.spawnQuantity % corners.size() );
								int cx = spawnDetails[0];
								int cy = spawnDetails[1];
								int startAngle = spawnDetails[2];
								int endAngle = spawnDetails[3];
								
								int angle = startAngle + Math.abs( this.random.nextInt() % (endAngle - startAngle) );
								int radius = Math.abs( random.nextInt() % MAX_SPAWN_DISTANCE );
								
								int dx = MathFP.mul( MathFP.cos( angle ), radius );
								int dy = MathFP.mul( MathFP.sin( angle ), radius );
								ex = cx + dx - width/2;
								ey = cy + dy - height/2;
								
								// TODO : check that this isn't too close to the player
								int dpx = ex - px;
								int dpy = ey - py;
								surround =
										MathFP.mul(dpx, dpx) + 
										MathFP.mul(dpy, dpy) < 
										MathFP.mul( MIN_SPAWN_BUFFER, MIN_SPAWN_BUFFER );
							}
							else
							{
								ex = 0;
								ey = 0;
								surround = true;
							}
							if( surround )
							{
								// surround the player
								int angle = Math.abs( random.nextInt() % (MathFP.PI * 2) );
								int distance = MIN_SPAWN_BUFFER + 
									Math.abs( this.random.nextInt() % 
									(MAX_SPAWN_DISTANCE - MIN_SPAWN_BUFFER) );
									
								int dx = MathFP.mul( distance, MathFP.cos( angle ) );
								int dy = MathFP.mul( distance, MathFP.sin( angle ) );
								ex = px + dx + width/2;
								ey = py + dy + height/2;
							}
							
							GWEntity enemy = new GWEntity(
									ex, ey,
									width, height,
									type
							); 
							this.enforceBounds( enemy );
							//if( this.enforceBounds( enemy ) == 0 )
							{
								this.layers[ this.layers.length - 1 ].addElement( 
									enemy
								);
							}
						}
					}
				}
				else if(this.layers[ GWEntity.TYPE_BLACKHOLE ].size() != 0)
				{
					this.spawnQuantity = 0;
				}
			}
			else
			{
				// new enemy types should be available every 30 seconds
				int maxEnemyId = Math.min( 
						GWEntity.MAX_ENEMY_ID, 
						( this.updateNumber * this.delay ) / ( 30 * 1000 ) + GWEntity.MIN_ENEMY_ID 
				);
				byte type;
				type = (byte)(GWEntity.MIN_ENEMY_ID + 
					Math.abs( 
							this.random.nextInt() % 
							(maxEnemyId - GWEntity.MIN_ENEMY_ID + 1) 
					));
				//type = GWEntity.TYPE_WORM;
				int weight = GWEntity.getWeight( type ); 
				this.spawnType = type; 
				int minutes = this.updateNumber / (DEFAULT_TARGET_FPS * 60) + 1; 
				this.spawnCountDown = Math.abs( this.random.nextInt() ) % ((MAX_SPAWN_DELAY - MIN_SPAWN_DELAY)/minutes) + MIN_SPAWN_DELAY;
				this.wave ++;
				if( type == GWEntity.TYPE_BLACKHOLE )
				{
					this.spawnQuantity = 1;
					this.spawnThroughput = 1;
				}
				else
				{
					this.spawnQuantity = Math.min( 
								( (minutes * this.spawnCountDown * this.spawnCountDown) )
								 / 
								weight + 1
							, MAX_SPAWN_QUANTITY
					);
					if( this.wave % (30 - minutes + 1 ) == 0 )
					{
						// bonus wave!!
						this.spawnQuantity = Math.min( this.spawnQuantity * minutes, MAX_SPAWN_QUANTITY );
					}
					//int maxThroughput = Math.max( 1, ( this.updateNumber * this.delay ) / ( ((128 - points)/6) * 1000 )  );
					int maxThroughput = 4;
					this.spawnThroughput = Math.abs( random.nextInt() % maxThroughput ) + 1;  
				}
				
				this.spawnStyle = Math.abs(random.nextInt() % 16);
			}
		}
		
		Graphics g = this.getGraphics();
		/*
		g.setColor( 0x000000 );
		g.fillRect( 0, 0, this.getWidth(), this.getHeight() );
		g.setColor( 0x4444AA );
		int gridSpacing = MathFP.toFP( 15 );
		for( int x=(gridSpacing - this.viewX % gridSpacing) - gridSpacing; x<this.viewX + this.screenWidth; x+=gridSpacing )
		{
			int sx = MathFP.toInt( x );
			g.drawLine( sx, 0, sx, this.getHeight() );
		}

		for( int y=(gridSpacing - this.viewY % gridSpacing) - gridSpacing; y<this.viewY + this.screenHeight; y+=gridSpacing )
		{
			int sy = MathFP.toInt( y );
			g.drawLine( 0, sy, this.getWidth(), sy );
		}
		*/
		int vx = toInt( this.viewX );
		int vy = toInt( this.viewY );
		//g.setColor( 0x000000 );
		//g.fillRect(0, 0, this.getWidth(), this.getHeight() );
		//g.setClip( Math.max(-vx, 0), Math.max(-vy, 0), this.getWidth(), this.getHeight() );
		g.drawImage( 
				this.backgroundImage, 
				toInt((gridSpacing - this.viewX % gridSpacing) - gridSpacing),
				toInt((gridSpacing - this.viewY % gridSpacing) - gridSpacing),
				Graphics.TOP | Graphics.LEFT
		);
		
		int lastBomb = this.playerController.lastBomb;
		if( lastBomb != 0 )
		{
			int diff = this.updateNumber - lastBomb;
			int max = 5;
			if( diff < (max) )
			{
				int radius;
				int divisor;
				if( diff > max )
				{
					divisor = (diff - max);
				}
				else
				{
					divisor = (max - diff) + 1;
				}
				radius = Math.max( this.getWidth(), this.getHeight() ) / divisor;
				
				int cx = toInt( this.playerController.lastBombPosition[0] - this.viewX );
				int cy = toInt( this.playerController.lastBombPosition[1] - this.viewY );
				for( int i=0; i<BOMB_COLORS.length; i++ )
				{
					int r = radius - i;
					int color = BOMB_COLORS[ i ];
					g.setColor( color );
					g.drawArc( cx - r, cy - r, r * 2, r * 2, 0, 360 );
					g.drawArc( cx - r, cy - r, r * 2-1, r * 2-1, 0, 360 );
				}
			}
		}
		
		g.setColor( 0x33DDFF );
		if( vx <= 0 )
		{
			g.drawLine( -vx, 0, -vx, this.getHeight() );
		}
		if( vy <= 0 )
		{
			g.drawLine( 0, -vy, this.getWidth(), -vy );
		}
		if( this.viewX >= this.levelWidth - this.screenWidth )
		{
			g.drawLine( this.getWidth()-1, 0, this.getWidth()-1, this.getHeight() );
		}
		
		if( this.viewY >= this.levelHeight - this.screenHeight )
		{
			g.drawLine( 0, this.getHeight()-1, this.getWidth(), this.getHeight()-1 );
		}
		
		for( int i=0; i<animationStyle.length; i++ )
		{
			boolean animate = animationStyle[ i ] == 0;
			if( animate )
			{
				Sprite[] sprites = this.sprites[ i ];
				for( int j=0; j<sprites.length; j++ )
				{
					Sprite sprite = sprites[ j ];
					sprite.nextFrame();
				}
			}
		}
		
		if( this.explodeAll )
		{
			for( int i=layers.length; i>0; )
			{
				i--;
				if( i != layers.length - 2 )
				{
					Vector layer = this.layers[i];
					for( int j=layer.size(); j>0; )
					{
						j--;
						GWEntity entity = (GWEntity) layer.elementAt( j );
						if( entity != this.explodeExcept )
						{
							layer.removeElementAt( j );
							this.explode( entity, false );
						}
					}
				}
			}
			this.explodeAll = false;
			this.explodeExcept = null;
		}

		this.updateNumber++;
		for( int i=layers.length; i>0; )
		{
			i--;
			
			
			int controllerType = i;
			Controller controller = this.controllers[ controllerType ];
			Vector layer = this.layers[i];
			
			if( i == GWEntity.TYPE_PLAYER )
			{
				Vector playerLayer = layer;
				if( playerLayer.size() == 0 )
				{
					if( this.playerController.playerLives > 0 )
					{
						if( this.updateNumber > this.lastDeath + UPDATE_PAUSE || this.lastDeath == 0 )
						{
							// clear it out
							for( int j=layers.length; j>0; )
							{
								j--;
								layers[j].removeAllElements();
							}
							
							// initialise
							GWEntity player = new GWEntity(
									this,
									this.viewX + this.screenWidth/2, 
									this.viewY + this.screenHeight/2,
									GWEntity.TYPE_PLAYER 
							);
 
							int playerWidth = player.width;
							int playerHeight = player.height;
							player.x -= playerWidth/2;
							player.y -= playerHeight/2;
							this.blackHole = null; 
							playerLayer.addElement( player );
						}
					}
					else if( this.mode > 0 )
					{
						this.mode = MODE_GAME_OVER;
					}
				}
			}
			
			if( layer.size() > 0 )
			{
				// TODO : some controllers rely on others having learned
				controller.learn( this );
				boolean gravity = this.layerGravity[ i ]; 
				for( int j=layer.size(); j>0; )
				{
					j--;
					GWEntity entity = (GWEntity)layer.elementAt( j );
					if( controller.update( this, entity ) )
					{
						// it's died!
						layer.removeElementAt( j );
					}
					else
					{
						if( gravity )
						{
							this.applyGravity( entity );
						}
						boolean skip = false;
						int k = i; // we only check below our current level!!
						int layerCollisionMask = this.layerCollisionMasks[i];
						outer: while( layerCollisionMask != 0 && k > 0 )
						{
							k--;
							
							if( (layerCollisionMask & 0x01) != 0 )
							{
								// check for collisions
								int collisionControllerType = k;
								Controller collisionController = this.controllers[ collisionControllerType ];
								Vector collisionLayer = this.layers[k];
								for( int l=collisionLayer.size(); l>0; )
								{
									l--;
									GWEntity collisionEntity = (GWEntity) collisionLayer.elementAt(l);
									if( this.intersects( 
											entity.x, entity.y, entity.width, entity.height, 
											collisionEntity.x, collisionEntity.y, 
											collisionEntity.width, collisionEntity.height
										)
									)
									{
										if( collisionController.collision( this, collisionEntity, entity ) )
										{
											collisionLayer.removeElementAt( l );
										}
										if( controller.collision( this, entity, collisionEntity ) )
										{
											layer.removeElementAt( j );
											// we're dead, bail out
											skip = true;
											break outer;
										}
									}
								}
							}
							layerCollisionMask >>= 1;
						}
						// render the entity, if it's not dead by now, it's OK
						if( !skip && intersects( 
								this.viewX, this.viewY, this.screenWidth, this.screenHeight, 
								entity.x, entity.y, entity.width, entity.height )
						)
						{
							int animationStyle = this.animationStyle[ entity.type ];
							
							Sprite sprite;
							int ex = toInt(entity.x + entity.width/2 - this.viewX);
							int ey = toInt(entity.y + entity.height/2 - this.viewY);
							
							if( i == this.layers.length - 1 )
							{
								int[] controllerData = (int[])entity.controllerData;
								g.setColor( controllerData[4] );
								g.fillRect( 
										toInt( entity.x - this.viewX ) + 1, 
										toInt( entity.y - this.viewY ) + 1, 
										toInt( entity.width ) - 1, 
										toInt( entity.height ) - 1 
								);
							}
							switch( animationStyle )
							{
							default:
							case 0:
								if( i == this.layers.length - 1 )
								{
									sprite = this.sprites[ entity.type ][1];
								}
								else
								{
									sprite = this.sprites[ entity.type ][0];
								}
								break;
							case 1:
								{
									// choose the sprite based on rotation
									int currentRotation = ((int[])entity.controllerData)[0];
									while( currentRotation < 0 )
									{
										currentRotation += (MathFP.PI * 2);
									}
									currentRotation = currentRotation % (MathFP.PI * 2);
									int index = (currentRotation * 2) / MathFP.PI;
									sprite = this.sprites[ entity.type ][ index ];
									int frame =  
											(sprite.getFrameSequenceLength() * ( currentRotation % ( MathFP.PI / 2 ) )) /  
											(MathFP.PI / 2);
									
									sprite.setFrame( frame );
	//								if( entity.type == 9 )
	//								{
	//									System.out.println( "current rotation "+MathFP.toInt( currentRotation ) +" => "+ frame+":"+index );
	//								}
								}
								break;
							case 2:
								{
									int[] controllerData = (int[])entity.controllerData;
									int cannonicalAngle = controllerData[2];
									sprite = this.sprites[ entity.type ][ cannonicalAngle ];
									int currentFrame = controllerData[0];
									sprite.setFrame( currentFrame );
									int score = controllerData[3];
									if( score > 0 )
									{
										String message = Integer.toString( score );
										g.setFont( SCORE_FONT );
										int color = controllerData[4];
										g.setColor( color );
										g.drawString( message, ex, ey + SCORE_FONT.getBaselinePosition()/2, Graphics.BASELINE | Graphics.HCENTER );
									}
								}
								break;
							case -1:
								int[] controllerData = (int[])entity.controllerData;
								sprite = this.sprites[ entity.type ][ 0 ];
								int currentFrame = controllerData[ 0 ];
								sprite.setFrame( currentFrame );
								break;
							}
							int sx = ex - sprite.getWidth()/2;
							int sy = ey - sprite.getHeight()/2;
							sprite.setPosition( sx, sy );
							sprite.paint( g );
							/*
							g.setColor( 0xFFFFFF );
							g.drawArc( 
									MathFP.toInt(entity.x - this.viewX), 
									MathFP.toInt(entity.y - this.viewY), 
									MathFP.toInt(entity.width), 
									MathFP.toInt(entity.height), 
									0, 360
							);
							*/
						}
					}
				}
			}
		}
		
		this.highScore = Math.max( this.highScore, this.playerController.getScore() );
		
		int scorey = SCORE_FONT.getBaselinePosition(); 
		g.setColor( 0xFFFFFF );
		g.setFont( SCORE_FONT );
		if( this.mode == MODE_GAME_OVER || this.mode == MODE_PLAYING )
		{
			g.drawString( 
					Integer.toString( this.playerController.getScore() ), 
					4, scorey, 
					Graphics.LEFT | Graphics.BASELINE 
			);
		}		
		g.drawString(
				Integer.toString( this.highScore ),
				this.getWidth() - 4, scorey, 
				Graphics.RIGHT | Graphics.BASELINE
		);
		
		g.setFont( MAIN_FONT );
		switch( this.mode )
		{
			case MODE_GAME_OVER:
				g.drawString( "Game Over", this.getWidth()/2, this.getHeight()/2, Graphics.HCENTER | Graphics.BASELINE );
				break;
			case MODE_CLICK_TO_PLAY:
				g.drawString( "Click to Play", this.getWidth()/2, this.getHeight()/2, Graphics.HCENTER | Graphics.BASELINE );
				break;
			case MODE_INSTRUCTIONS:
				g.drawString( "Instructions", this.getWidth()/2, this.getHeight()/2, Graphics.HCENTER | Graphics.BASELINE );
				break;
			case MODE_SCORES:
				g.drawString( "High Scores", this.getWidth()/2, this.getHeight()/2, Graphics.HCENTER | Graphics.BASELINE );
				break;
			case MODE_PLAYING:
				Sprite bombIcon = this.sprites[ this.sprites.length - 2 ][0];
				renderIcon( 
						g, bombIcon, 
						this.getWidth()/2 - bombIcon.getWidth() * this.maxIcons, 
						3, this.playerController.playerBombs 
				);
				Sprite playerIcon = this.sprites[ this.sprites.length - 1 ][0];
				renderIcon( 
						g, playerIcon, 
						this.getWidth()/2, 
						3, this.playerController.playerLives 
				);
				g.setFont( INFO_FONT );
				g.drawString( 
						"next power up at "+this.playerController.nextPowerUpScore, 
						3, this.getHeight() - INFO_FONT.getHeight() + INFO_FONT.getBaselinePosition(), 
						Graphics.LEFT | Graphics.BASELINE 
				);
				g.drawString(
						"x"+this.playerController.multiplier,
						this.getWidth() - 3, this.getHeight() - INFO_FONT.getHeight() + INFO_FONT.getBaselinePosition(),
						Graphics.RIGHT | Graphics.BASELINE
				);
				break;
		}
	}
	
	private void renderIcon( Graphics g, Sprite icon, int x, int y, int number )
	{
		int max = this.maxIcons;
		if( number > max )
		{
			icon.setPosition( x, y );
			icon.paint( g );
			g.drawString( 
					Integer.toString( number ), 
					icon.getX() + icon.getWidth(),
					icon.getY() + SCORE_FONT.getBaselinePosition(), 
					Graphics.LEFT | Graphics.BASELINE 
			);
		}
		else
		{
			for( int i=0; i<number; i++ )
			{
				icon.setPosition( x + i * icon.getWidth(), y );
				icon.paint( g );
			}
		}
	}
	
	public void run()
	{
		while( this.running )
		{
			long currentTime = System.currentTimeMillis();
			try
			{
				this.updateGame();
			}
			catch( Exception ex )
			{
				ex.printStackTrace();
			}
			this.flushGraphics();
			long sleepFor = currentTime - System.currentTimeMillis() + this.delay;
			this.slowing = sleepFor <= 0;
			if( !this.slowing )
			{
				try
				{
					Thread.sleep( sleepFor );
				}
				catch( Exception ex )
				{
					// do nothing
				}
			}
		}
	}
	
	public void applyGravity( GWEntity o ) 
	{
		if( this.blackHole != null )
		{
			// TODO : avoid using MathFP methods
			int[] controllerData = (int[])this.blackHole.controllerData;
			int active = controllerData[3];
			if( active != 0 )
			{
				int gravity = controllerData[2];
				int bx = this.blackHole.x + this.blackHole.width/2;
				int by = this.blackHole.y + this.blackHole.height/2;
				
				int ox = o.x + o.width/2;
				int oy = o.y + o.height/2;
				int dx = bx - ox;
				int dy = by - oy;
				//int distance = MathFP.sqrt( MathFP.mul( dx, dx ) + MathFP.mul( dy, dy ) );
				int distance = Math.max( Math.abs( dx ), Math.abs( dy ) );
				if( distance < this.levelHeight/4 )
				{
					int speed = MathFP.div(gravity * this.levelHeight/32, distance );
					dx = MathFP.div(MathFP.mul(dx, speed), distance);
					dy = MathFP.div(MathFP.mul(dy, speed), distance);
					int maxSpeed = this.playerController.SPEED/2;
					//o.x += Math.max( -maxSpeed, Math.min( dx, maxSpeed ) );
					//o.y += Math.max( -maxSpeed, Math.min( dy, maxSpeed ) );
					dx = Math.max( -maxSpeed, Math.min( dx, maxSpeed ) );
					dy = Math.max( -maxSpeed, Math.min( dy, maxSpeed ) );
					if( o.type == GWEntity.TYPE_BULLET && ( dx != 0 || dy != 0 ) )
					{
						int[] bulletData = (int[]) o.controllerData;
						int vdx = bulletData[1]-dx;
						int vdy = bulletData[2]-dy;
						// ensure that the total isn't 0,0
						try
						{
							int rotation = GWLiteCanvas.atan2( vdy, vdx );
							bulletData[0] = rotation;
							bulletData[1] = MathFP.cos( rotation ) * this.playerController.bulletSpeed;
							bulletData[2] = MathFP.sin( rotation ) * this.playerController.bulletSpeed;
						}
						catch( Exception ex )
						{
							o.x += dx;
							o.y += dy;
						}
					}
					else
					{
						o.x += dx;
						o.y += dy;
					}
				}
			}
		}
	}

	protected void keyPressed(int keyCode) 
	{
		super.keyPressed( keyCode );
		if( this.mode <= 0 && ( this.updateNumber > this.lastDeath + UPDATE_PAUSE || this.lastDeath == 0 ) )
		{
			reset();
		}
		this.playerController.setKeyState( keyCode, true );
	}

	protected void keyReleased(int keyCode) 
	{
		super.keyReleased( keyCode );
		this.playerController.setKeyState( keyCode, false );
	}
	
	public int enforceBounds( GWEntity o )
	{
		int wall = 0;
		if( o.x < 0 )
		{
			o.x = 0;
			wall |= WALL_WEST;
		}
		else if( o.x + o.width > this.levelWidth )
		{
			o.x = this.levelWidth - o.width;
			wall |= WALL_EAST;
		}
		if( o.y < 0 )
		{
			o.y = 0;
			wall |= WALL_NORTH;
		}
		else if( o.y + o.height > this.levelHeight )
		{
			o.y = this.levelHeight - o.height;
			wall |= WALL_SOUTH;
		}
		return wall;
	}
	
	public boolean intersects( 
			int x1, int y1, int w1, int h1, 
			int x2, int y2, int w2, int h2 
	)
	{
		return (x1 + w1 >= x2 && x1 < x2 + w2 ||
				x2 + w2 >= x1 && x2 < x1 + w1 ) &&
			   (y1 + h1 >= y2 && y1 < y2 + h2 ||
			    y2 + h2 >= y1 && y2 < y1 + h1 );
	}
	
	public void explode( GWEntity entity )
	{
		explode( entity, true );
	}
	
	public void explode( GWEntity entity, boolean countScore )
	{
		int x = entity.x + entity.width/2;
		int y = entity.y + entity.height/2;
		int index = this.layers.length - 2;
		int score;
		int explosions;
		if( countScore )
		{
			explosions = GWEntity.getPoints( entity.type ) / 8;
			score = GWEntity.getPoints( entity.type ) * this.playerController.multiplier; 
		}
		else
		{
			score = 0;
			explosions = 0;
		}
		if( !this.slowing )
		{
			for( int i=0; i<explosions; i++ )
			{
				int dx = this.random.nextInt() % entity.width;
				int dy = this.random.nextInt() % entity.height;
				GWEntity explosion = new GWEntity(
						this,
						x + dx,
						y + dy, 
						GWEntity.TYPE_THRUST 
				);
				(( StunController )this.controllers[ index ]).initialise( this, explosion, 0, dx, dy );
				this.layers[ index ].addElement( explosion );
			}
		}
		GWEntity explosion = new GWEntity(
				this,
				x + this.random.nextInt() % entity.width, 
				y + this.random.nextInt() % entity.height, 
				GWEntity.TYPE_EXPLOSION 
		);
		(( StunController )this.controllers[ index ]).initialise( this, explosion, score, 0, 0 );
		this.layers[ index ].addElement( explosion );
		if( score > 0 )
		{
			this.playerController.addScore( score );
		}
	}

	protected void pointerDragged(int x, int y) 
	{
		this.playerController.target( x, y );
	}

	protected void pointerPressed(int x, int y) 
	{
		this.playerController.target( x, y );
	}

	protected void pointerReleased(int x, int y) 
	{
		this.playerController.untarget( x, y );
	}
	
	public static final int atan2( int dy, int dx )
	{
		int rotation;
		if( dy < 0 && dx <= 0 )
		{
			// there's a bug
			rotation = -MathFP.atan2( dy, dx );
		}
		else
		{
			rotation = MathFP.atan2( dy, dx );
		}
		return rotation;
	}
	
	public static final int toInt( int i )
	{
		return i >> MathFP.getPrecision();
	}
	
	public void setExplodeAll( GWEntity except )
	{
		this.explodeAll = true;
		this.explodeExcept = except;
		this.blackHole = null;
	}
	
	private final void reset()
	{
		// clear out all the layers
		for( int i=layers.length; i>0; )
		{
			i--;
			Vector layer = this.layers[i];
			Controller controller = this.controllers[i];
			controller.reset();
			layer.removeAllElements();
		}
		// do some stuff
		this.mode = MODE_PLAYING;
		this.updateNumber = 0;
		this.lastDeath = 0;
		
		this.blackHole = null;
		this.spawnCountDown = 0;
		this.spawnQuantity = 0;
		
	}
}
